Conn = Net::HTTP.start('www.neopets.com')

class AutoTrainer
  def initialize
    @window = Window.new
    @window.window_position = Window::POS_CENTER
    @window.title = "Inkheart's Autotrainer"
    @window.signal_connect('destroy') { main_quit }
    Net::HTTP.start('i.imgur.com') do |http|
      icon = ChunkyPNG::Image.from_string http.get('/duGdK.png').body
      @window.icon = Pixbuf.new icon.to_rgba_stream, 0, true, 8, 96, 96, 384
    end

    @headers = {'User-Agent' => 'Mozilla/5.0'}

    @app = VBox.new
    @pets_area = HBox.new
    @app.pack_end @pets_area
    @pets_area.hide
    @window.add @app

    @info_scroller = ScrolledWindow.new
    @info_scroller.set_policy POLICY_NEVER, POLICY_AUTOMATIC
    @info_box = TextView.new
    @info_box.set_wrap_mode TextTag::WRAP_WORD
    @info_box.width_request = 200
    @info_box.editable = @info_box.cursor_visible = false
    @info = @info_box.buffer
    @info_scroller.add @info_box

    @options = Window.new
    @options.modal = true
    @options.decorated = false
    @options.window_position = Window::POS_MOUSE
    @options.title = 'Training Strategy'
    opt_box = VBox.new(false, 5)
    opt_box.width_request = 250
    opt_box.pack_start (@strat_for = Label.new('Strategy'))
    opt_box.pack_start (balanced = RadioButton.new('Balanced'))
    balanced.tooltip_text = 'Maintain 2/1 ratio between level and the stats selected below.'
    opt_box.pack_start (random = RadioButton.new(balanced, 'Random'))
    random.tooltip_text = 'Randomly train the stats selected below. Level will be trained automatically if required.'
    opt_box.pack_start HSeparator.new
    choices = Table.new(3, 2)
    choices.attach CheckButton.new('Level'), 0, 1, 0, 1
    choices.attach CheckButton.new('HP'), 1, 2, 0, 1
    choices.attach CheckButton.new('Strength'), 0, 1, 1, 2
    choices.attach CheckButton.new('Defense'), 1, 2, 1, 2
    choices.attach CheckButton.new('Movement'), 0, 1, 2, 3
    opt_box.pack_start choices
    opt_box.pack_start HSeparator.new
    opt_box.pack_start (set_order = RadioButton.new(balanced, 'Set Order'))
    set_order.tooltip_text = 'Use LSDMH to specify the order in which to train stats. Level will be trained automatically if required.'
    opt_box.pack_start (order = Entry.new)
    opt_box.pack_start HSeparator.new
    button_box = HBox.new
    button_box.pack_start (go = Button.new('Go!'))
    button_box.pack_start (cancel = Button.new('Cancel'))
    opt_box.pack_start button_box
    opt_frame = Frame.new
    opt_frame.add opt_box
    opt_frame.border_width = 5
    @options.add opt_frame
    go.signal_connect('clicked') do
      @options.hide_all
      @train_buttons[@active].label = 'Stop Training'
      @training[@active] = true
      strat = balanced.group.find_index { |r| r.active? }
      stats = {'L' => 'Level', 'H' => 'Endurance', 'S' => 'Strength', 'D' => 'Defence', 'M' => 'Agility'}
      if strat == 0
        @strategy[@active] = order.text.split(/\s*/).map { |s| stats.fetch(s, nil) }.compact
      else
        use_stats = choices.children.reverse.map(&:active?)
        use_stats = (0..4).map { |i| use_stats[i] ? stats.values[i] : nil}.compact
        use_stats = ['Level', 'Endurance', 'Strength', 'Defence', 'Agility'] if use_stats.empty?
        @strategy[@active] = [strat == 1 ? 'random' : 'balanced', use_stats]
      end
      add_info "Set strategy for #{@active}: #{@strategy[@active]}"
    end
    cancel.signal_connect('clicked') { @options.hide_all }
  end

  def add_info(str)
    @info.insert_at_cursor "\n#{str}\n"
    @info_box.scroll_mark_onscreen @info.create_mark('end', @info.end_iter, true)
  end

  def login(un, pw, initial)
    resp = Conn.post('/login.phtml', "username=#{un}&password=#{pw}", @headers)
    cookie = resp.get_fields('Set-Cookie').map { |c| c.split(';')[0] } * ';' rescue ''
    if cookie.include? 'neoremember'
      @un, @pw = un, pw
      @headers['Cookie'] = cookie
      display_pets if initial
    else
      @login_status.markup = '<span foreground="red">Login failed.</span>'
    end
  end

  def display_login
    login_area = HBox.new
    login_area.name = 'login'
    login_area.pack_start Label.new 'Username: '
    login_area.pack_start(username = Entry.new)
    login_area.pack_start Label.new 'Password: '
    login_area.pack_start(password = Entry.new)
    password.visibility = false
    login_area.pack_start(login_button = Button.new('Log In'))
    login_button.signal_connect('clicked') do
      login(username.text, password.text, true)
    end
    @app.pack_start (@login_area = login_area)
    @app.pack_start (@login_status = Label.new('Waiting for credentials...'))
  end

  def display_pets
    [@login_area, @login_status].map(&:destroy) if @login_area

    pet_images = Net::HTTP.start('pets.neopets.com')
    stats = %w[Studying Level Strength Defense Movement HP]
    status = Conn.get('/island/training.phtml?type=status', @headers).body
    pets = status.scan(/colspan=2><b>(.+?) .+?\).+? (\w+?)<.+?>(\d+)<.+?>(\d+)<.+?>(\d+)<.+?>(\d+)<.+?>([\d\/ ]+?)<.+?align=center>(.*?)<\/td/)
    add_info "Logged in as #{@un}."

    @stat_labels, @train_buttons = {}, {}
    @training, @strategy, @status, @studying, @price, @data, @time = {}, {}, {}, {}, {}, {}, {}

    pets.each do |data|
      name, studying, level, strength, defense, movement, hp, status = data
      data[6] = data[6].split(' / ')[1]
      @data[name] = data
      studying = 'Nothing' if studying == 'course'
      @studying[name] = studying

      status = 'On Standby' if status == ''
      status = 'Course Finished' if status[/Finished!/]

      unless (@price[name] = status.scan(/>([^<>]+ Codestone)</).flatten).empty?
        status = "Awaiting Payment"
      end

      if status.include? 'Time till'
        hr, mn, sc = status.scan(/\d+/).flatten.map(&:to_i)
        @time[name] = Time.now + hr * 3600 + mn * 60 + sc
        status = 'Training (unwatched)'
      end

      pet_box = VBox.new
      pet_box.name = "#{name}_box"
      pet_url = pet_images.get("/cpn/#{name}/2/2.png")['location']
      img = ChunkyPNG::Image.from_string(pet_images.get(pet_url).body)
      pet_box.pack_start Image.new(Pixbuf.new(img.to_rgba_stream, 0, true, 8, 150, 150, 600))
      pet_box.pack_start Label.new name
      stats.each do |stat|
        @stat_labels[name + stat] = Label.new "#{stat}: #{eval(stat.downcase)}"
        pet_box.pack_start @stat_labels[name + stat]
      end
      pet_box.pack_start Label.new "\nStatus"
      @status[name] = Label.new status
      pet_box.pack_start @status[name]
      pet_box.pack_start (@train_buttons[name] = Button.new('Start Training'))
      @train_buttons[name].signal_connect('clicked') do |this|
        if this.label == 'Stop Training'
          @training[name] = nil
          @status[name].text = 'Training (unwatched)'
          this.label = 'Start Training'
        else
          @active = name
          @strat_for.text = "Strategy for\n#{name}"
          @options.show_all
        end
      end
      @pets_area.pack_start pet_box
    end

    @pets_area.pack_start @info_scroller
    @pets_area.show_all
    pet_images.finish
    timeout_add(1000) { update }
  end

  def get_codestones(pet)
    need = @price[pet]
    p need
    inventory = Conn.get('/objects.phtml?type=inventory', @headers).body
    np = inventory[/inventory">(.+?)</, 1].gsub(/[^\d]/, '').to_i
    items = inventory.scan(/tItem">.+?([^<>]+)</).flatten
    need.uniq.each do |n|
      [items.count(n), need.count(n)].min.times { need.delete_at need.index(n) }
    end
    return true if need.empty?
    p need
    sleep 1

    sdb = Conn.get('/safetydeposit.phtml?obj_name=&category=2', @headers).body
    items, sdb = sdb.scan(/>([^<>]+ Codestone).+?>(\d+)<.+?\[(\d+)\]/m), {}
    items.each { |i| sdb[i.shift] = i.map(&:to_i) }
    back_to_inv = {}
    sdb.values.each { |v| back_to_inv[v[1]] = 0 }
    need.uniq.each do |n|
      if sdb[n]
        back_to_inv[sdb[n][1]] = [need.count(n), sdb[n][0]].min
        back_to_inv[sdb[n][1]].times do
          need.delete_at need.index(n)
        end
      end
    end
    data = 'obj_name=&offset=0&category=2'
    data += '&' + back_to_inv.sort.map { |b| "back_to_inv[#{b[0]}]=#{b[1]}"} * '&'
    Conn.post('/process_safetydeposit.phtml?checksub=scan', data, @headers.merge('Referer' => 'http://www.neopets.com/safetydeposit.phtml?obj_name=&category=2'))
    return true if need.empty?
    p need
    sleep 1

    need.uniq.each do |n|
      p n
      wiz = Conn.post('/market.phtml', "type=process_wizard&feedset=0&shopwizard=#{n}&table=shop&criteria=exact&min_price=0&max_price=99999", @headers.merge('Referer' => 'http://www.neopets.com/market.phtml?type=wizard')).body
      shops = wiz.scan /(browseshop.phtml.+?)".+?>(\d+)</
      target = shops.find { |sh| sh[1].to_i > need.count(n) }[0]
      shop_html = Conn.get('/' + target, @headers).body
      buy_link = shop_html[/(buy_item.phtml?.+?)"/, 1]
      price = buy_link[/old_price=(\d+)/, 1].to_i
      need.count(n).times do
        p '...'
        if price > np
         add_info "Not enough NP to purchase the required Codestones for #{pet}'s #{@studying[pet]} course."
          @status[pet].text = 'Awaiting Payment'
          @train_buttons[pet].label = 'Start Training'
          @training[pet] = false
          return false
        else
          Conn.get('/' + buy_link, @headers.merge('Referer' => 'http://www.neopets.com/' + target))
          np -= price
          need -= [n]
        end
      end
    end
    return true
  end

  def ensure_login
    login(@un, @pw) if Conn.head('/', @headers)['Set-Cookie'].include? 'np_randseed'
    p 'logged in'
    return true
  end

  def update
    @status.each do |p, s|
      next unless @training[p]

      if @time[p]
        rem = (@time[p] - Time.now).to_i
        if rem <= 0
          s.text = 'Course Finished'
          @time[p] = nil
          next
        else
          s.text = [rem / 3600, (rem % 3600) / 60, rem % 60].map { |e| '%02d' % e } * ':'
        end
      end

      if s.text == 'On Standby'
        ensure_login
        stat = nil
        add_info "#{p} is on standby, should be training."
        level, strength, defense, movement, hp = @data[p][2..6].map(&:to_i)

        restrict = [strength, defense, movement, hp].any? { |st| st * 2 > level }
        if restrict
          add_info "#{p} is restricted to training Level."
          stat = 'Level'
        end

        if @strategy[p][0] == 'balanced'
          stat_vals = {'Strength' => strength, 'Defence' => defense, 'Agility' => movement, 'Endurance' => hp}
          @strategy[p][1] -= ['Level']
          stat_vals.each { |k, v| stat_vals.delete k unless @strategy[p][1].include? k }
          stat ||= stat_vals.invert.min[1]
        elsif @strategy[p][0] == 'random'
          stat ||= @strategy[p][1].sample
        else
          stat ||= @strategy[p].shift
        end
        unless stat
          @training[p] = false
          @train_buttons[p].label = 'Start Training'
          next
        end

        add_info "Starting #{stat} course for #{p}..."
        resp = Conn.post('/island/process_training.phtml', "type=start&course_type=#{stat}&pet_name=#{p}", @headers.merge('Referer' => 'http://www.neopets.com/island/training.phtml?type=courses')).body
        if resp.include? 'Swashbuckling'
          @training[p] = false
          add_info "#{p} is currently in the Swashbuckling Academy and so can't be trained."
          next
        end

        html = Conn.get('/island/training.phtml?type=status', @headers).body.split(/#{p}.+?to pay\)/)[1].split('width=250')[0]
        @price[p] = html.scan(/>([^<>]+ Codestone)</).flatten
        @studying[p] = stat
        @stat_labels[p + 'Studying'].text = "Studying: #{stat}"
        add_info "Hang on, grabbing Codestones might take a bit."
        s.text = 'Awaiting Payment'
      end

      if s.text == 'Awaiting Payment'
        ensure_login
        next unless get_codestones(p)
        Conn.get("/island/process_training.phtml?type=pay&pet_name=#{p}", @headers.merge('Referer' => 'http://www.neopets.com/island/training.phtml?type=status'))
        html = Conn.get('/island/training.phtml?type=status', @headers).body
        hr, mn, sc = html[/#{p}.+?Time till.+?>([^<>]+)</, 1].scan(/\d+/).flatten.map(&:to_i)
        @time[p] = Time.now + hr * 3600 + mn * 60 + sc
        add_info "#{p} is now training #{@studying[p]}."
        next
      end

      if s.text == 'Course Finished'
        ensure_login
        resp = Conn.post('/island/process_training.phtml', "type=complete&pet_name=#{p}", @headers.merge('Referer' => 'http://www.neopets.com/island/training.phtml?type=status'))
        s.text = 'On Standby'
        next
      end
    end
  end

  def start
    display_login
    @window.show_all
    main
  end
end

AutoTrainer.new.start